Skip to main content

Simple Physics

A very simple physics script. If attached to an object it will have gravity, velocity, angular velocity and land on static voxel data. When it hits something it will always stop moving the object.

You can affect it via setVelocity(velocity, ?angularVelocityDegrees) and setEnabled(bool). You can also change .gravity and .height to affect the physics. The collision body is always a sphere with a diameter of 0.5 by default. Other scripts can add a listener to onHit to do something when it hits the ground.

This is a simple example. Physics will later be handled by the engine.

local self = {}

local enabled = true
self.vel = Vec3()
self.angVel = Vec3()
self.gravity = 20
self.sleeping = false

--everything is handled as a sphere with this diameter
self.height = 0.5


function self:Attach()
self.component.syncToClients = true

self.vr = self.object:GetComponentByType("VoxelRenderer")
local vd = self.object:GetComponentByType("VoxelData")
self.vd = vd and vd.data

--fires when hit the ground or other
---@class onHit:event
---@field addListener fun(script:ScriptInstance, listener:fun())
self.onHit, self.onHitTrigger = events:makeLocalEvent(self)
end

function self:Update(deltaTime)
if self.onClient then return end
if not enabled then return end
self:updateSleeping()
if self.sleeping then return end
self:updatePhysics()
end

function self:OnActivate()
--always wake up
self.sleeping = false
end

---@param velocity Vec3
---@param angularVelocityDegree ?Vec3
function self:setVelocity(velocity, angularVelocityDegrees)
self.vel = velocity
self.angVel = angularVelocityDegree or self.angVel
self.sleeping = false
end

function self:setEnabled(en)
enabled = en
--also wake up
if en then
self.sleeping = false
end
end

function self:updatePhysics()
if self.onClient then
return
end

local tf = self.transform
local dt = Scene:GetDeltaTime()

--add gravity
self.vel.y = self.vel.y - self.gravity * dt

--update pos, rot
--quick fix: limit dt to avoid falling through floor on lag
local dt = math.min(dt, 0.1)
tf.pos = tf.pos + self.vel * dt
tf.rot = Quat.Euler(self.angVel * dt) * tf.rot

self:checkCollisionsSimple()
end

--simple raycast down
function self:checkCollisionsSimple()
if self:getGrounded() then
local tf = self.transform
local dist = self:getDistToGround()
local underground = self.height * 0.5 - dist --dist is 0-0.5
tf.pos = tf.pos + underground * Vec3.up
self.sleeping = true
self.onHitTrigger()
end
end

function self:updateSleeping()
if not self.sleeping then return end
local p = self.transform.pos
local lp = self.lastPos or Vec3()
if (p-lp):SqrLength() > 0.1 then
self.sleeping = false
self.vel = Vec3()
self.lastPos = self.transform.pos
end
end

function self:getGrounded()
local grounded = self:getDistToGround() <= self.height * 0.5
return grounded
end

function self:getDistToGround()
local pos = self.transform.pos
local c = Collision()
c.filter = Filter()
c.filter.useNotStatic = false
local hit = c:Raycast(pos, Vec3.down * self.height)[1]
if not hit then return math.huge end
local dist = (hit.pos - pos):Len()
return dist
end

return self